<!DOCTYPE html>
<!--
Copyright (c) 2013 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->

<link rel="import" href="/tracing/base/guid.html">
<link rel="import" href="/tracing/base/math/range.html">
<link rel="import" href="/tracing/model/async_slice_group.html">
<link rel="import" href="/tracing/model/event_container.html">
<link rel="import" href="/tracing/model/slice_group.html">
<link rel="import" href="/tracing/model/thread_slice.html">

<script>
'use strict';

/**
 * @fileoverview Provides the Thread class.
 */
tr.exportTo('tr.model', function() {
  const AsyncSlice = tr.model.AsyncSlice;
  const AsyncSliceGroup = tr.model.AsyncSliceGroup;
  const SliceGroup = tr.model.SliceGroup;
  const ThreadSlice = tr.model.ThreadSlice;
  const ThreadTimeSlice = tr.model.ThreadTimeSlice;

  /**
   * A Thread stores all the trace events collected for a particular
   * thread. We organize the synchronous slices on a thread by "subrows," where
   * subrow 0 has all the root slices, subrow 1 those nested 1 deep, and so on.
   * The asynchronous slices are stored in an AsyncSliceGroup object.
   *
   * The slices stored on a Thread should be instances of
   * ThreadSlice.
   *
   * @constructor
   * @extends {tr.model.EventContainer}
   */
  function Thread(parent, tid) {
    if (!parent) {
      throw new Error('Parent must be provided.');
    }

    tr.model.EventContainer.call(this);
    this.parent = parent;
    this.sortIndex = 0;
    this.tid = tid;
    this.name = undefined;
    this.samples_ = undefined; // Set during createSubSlices

    this.sliceGroup = new SliceGroup(this, ThreadSlice, 'slices');
    this.timeSlices = undefined;
    this.kernelSliceGroup = new SliceGroup(
        this, ThreadSlice, 'kernel-slices');
    this.asyncSliceGroup = new AsyncSliceGroup(this, 'async-slices');
  }

  Thread.prototype = {
    __proto__: tr.model.EventContainer.prototype,

    get model() {
      return this.parent.model;
    },

    get stableId() {
      return this.parent.stableId + '.' + this.tid;
    },

    compareTo(that) {
      return Thread.compare(this, that);
    },

    * childEventContainers() {
      if (this.sliceGroup.length) {
        yield this.sliceGroup;
      }
      if (this.kernelSliceGroup.length) {
        yield this.kernelSliceGroup;
      }
      if (this.asyncSliceGroup.length) {
        yield this.asyncSliceGroup;
      }
    },

    * childEvents() {
      if (this.timeSlices) {
        yield* this.timeSlices;
      }
    },

    iterateAllPersistableObjects(cb) {
      cb(this);
      if (this.sliceGroup.length) {
        cb(this.sliceGroup);
      }
      this.asyncSliceGroup.viewSubGroups.forEach(cb);
    },

    /**
     * Shifts all the timestamps inside this thread forward by the amount
     * specified.
     */
    shiftTimestampsForward(amount) {
      this.sliceGroup.shiftTimestampsForward(amount);

      if (this.timeSlices) {
        for (let i = 0; i < this.timeSlices.length; i++) {
          const slice = this.timeSlices[i];
          slice.start += amount;
        }
      }

      this.kernelSliceGroup.shiftTimestampsForward(amount);
      this.asyncSliceGroup.shiftTimestampsForward(amount);
    },

    /**
     * Determines whether this thread is empty. If true, it usually implies
     * that it should be pruned from the model.
     */
    get isEmpty() {
      if (this.sliceGroup.length) return false;
      if (this.sliceGroup.openSliceCount) return false;
      if (this.timeSlices && this.timeSlices.length) return false;
      if (this.kernelSliceGroup.length) return false;
      if (this.asyncSliceGroup.length) return false;
      if (this.samples_.length) return false;
      return true;
    },

    /**
     * Updates the bounds based on the
     * current objects associated with the thread.
     */
    updateBounds() {
      this.bounds.reset();

      this.sliceGroup.updateBounds();
      this.bounds.addRange(this.sliceGroup.bounds);

      this.kernelSliceGroup.updateBounds();
      this.bounds.addRange(this.kernelSliceGroup.bounds);

      this.asyncSliceGroup.updateBounds();
      this.bounds.addRange(this.asyncSliceGroup.bounds);

      if (this.timeSlices && this.timeSlices.length) {
        this.bounds.addValue(this.timeSlices[0].start);
        this.bounds.addValue(
            this.timeSlices[this.timeSlices.length - 1].end);
      }

      if (this.samples_ && this.samples_.length) {
        this.bounds.addValue(this.samples_[0].start);
        this.bounds.addValue(
            this.samples_[this.samples_.length - 1].end);
      }
    },

    addCategoriesToDict(categoriesDict) {
      for (let i = 0; i < this.sliceGroup.length; i++) {
        categoriesDict[this.sliceGroup.slices[i].category] = true;
      }
      for (let i = 0; i < this.kernelSliceGroup.length; i++) {
        categoriesDict[this.kernelSliceGroup.slices[i].category] = true;
      }
      for (let i = 0; i < this.asyncSliceGroup.length; i++) {
        categoriesDict[this.asyncSliceGroup.slices[i].category] = true;
      }
      if (this.samples_) {
        for (let i = 0; i < this.samples_.length; i++) {
          categoriesDict[this.samples_[i].category] = true;
        }
      }
    },

    autoCloseOpenSlices() {
      this.sliceGroup.autoCloseOpenSlices();
      this.asyncSliceGroup.autoCloseOpenSlices();
      this.kernelSliceGroup.autoCloseOpenSlices();
    },

    mergeKernelWithUserland() {
      if (this.kernelSliceGroup.length > 0) {
        const newSlices = SliceGroup.merge(
            this.sliceGroup, this.kernelSliceGroup);
        this.sliceGroup.slices = newSlices.slices;
        this.kernelSliceGroup = new SliceGroup(this);
        this.updateBounds();
      }
    },

    createSubSlices() {
      this.sliceGroup.createSubSlices();
      this.samples_ = this.parent.model.samples.filter(sample =>
        sample.thread === this);
    },

    /**
     * @return {String} A user-friendly name for this thread.
     */
    get userFriendlyName() {
      return this.name || this.tid;
    },

    /**
     * @return {String} User friendly details about this thread.
     */
    get userFriendlyDetails() {
      return 'tid: ' + this.tid +
          (this.name ? ', name: ' + this.name : '');
    },

    getSettingsKey() {
      if (!this.name) return undefined;
      const parentKey = this.parent.getSettingsKey();
      if (!parentKey) return undefined;
      return parentKey + '.' + this.name;
    },

    getProcess() {
      return this.parent;
    },

    /*
     * Returns the index of the slice in the timeSlices array, or undefined.
     */
    indexOfTimeSlice(timeSlice) {
      const i = tr.b.findLowIndexInSortedArray(
          this.timeSlices,
          function(slice) { return slice.start; },
          timeSlice.start);
      if (this.timeSlices[i] !== timeSlice) return undefined;
      return i;
    },

    sumOverToplevelSlicesInRange(range, func) {
      let sum = 0;
      tr.b.iterateOverIntersectingIntervals(
          this.sliceGroup.topLevelSlices,
          slice => slice.start,    // mapLoFn
          slice => slice.duration, // mapWidthFn measures width not end of slice
          range.min, range.max,
          slice => {
            let fractionOfSliceInsideRangeOfInterest = 1;
            if (slice.duration > 0) {
              const intersection = range.findIntersection(slice.range);
              fractionOfSliceInsideRangeOfInterest =
                  intersection.duration / slice.duration;
            }
            // We assume that if a slice doesn't lie entirely inside the range
            // of interest, then |func| is evenly distributed inside of the
            // slice.
            sum += func(slice) * fractionOfSliceInsideRangeOfInterest;
          });
      return sum;
    },

    /**
     * Returns the total cpu time consumed within |range| by this thread.
     */
    getCpuTimeForRange(range) {
      return this.sumOverToplevelSlicesInRange(
          range, slice => slice.cpuDuration || 0);
    },

    /**
     * Returns the total number of top-level slices within |range| in this
     * thread. If a slice overlaps with |range| and is not completely inside it,
     * then we attribute the portion that is inside the range only. For example,
     * |getNumToplevelSlicesForRange| will return 1 + 1/3 when we have:
     *
     * 01  02  03  04  05  06  07  08  09 10
     *         <---------- range ---------->
     * <- slice #1 ->      <- slice #2 ->
     */
    getNumToplevelSlicesForRange(range) {
      return this.sumOverToplevelSlicesInRange(range, slice => 1);
    },

    getWallTimeForRange(range) {
      return this.sumOverToplevelSlicesInRange(
          range, slice => slice.duration || 0);
    },

    getSchedulingStatsForRange(start, end) {
      const stats = {};

      if (!this.timeSlices) return stats;

      function addStatsForSlice(threadTimeSlice) {
        const overlapStart = Math.max(threadTimeSlice.start, start);
        const overlapEnd = Math.min(threadTimeSlice.end, end);
        const schedulingState = threadTimeSlice.schedulingState;

        if (!(schedulingState in stats)) stats[schedulingState] = 0;
        stats[schedulingState] += overlapEnd - overlapStart;
      }

      tr.b.iterateOverIntersectingIntervals(this.timeSlices,
          function(x) { return x.start; },
          function(x) { return x.end; },
          start,
          end,
          addStatsForSlice);
      return stats;
    },

    get samples() {
      return this.samples_;
    },

    /**
     * Returns substring of this.name from beginning to the first numeric
     * character or the character '/'.
     *
     * Example:
     * ThreadName12      -> ThreadName
     * ThreadName/34123  -> ThreadName
     * ThreadName1/34123 -> ThreadName
     */
    get type() {
      const re = /^[^0-9|\/]+/;
      const matches = re.exec(this.name);
      if (matches && matches[0]) return matches[0];

      // If a thread is named 42GPU, let's not try to find its type.
      // We should fix the thread name.
      throw new Error('Could not determine thread type for thread name ' +
          this.name);
    }
  };

  /**
   * Comparison between threads that orders first by parent.compareTo,
   * then by names, then by tid.
   */
  Thread.compare = function(x, y) {
    let tmp = x.parent.compareTo(y.parent);
    if (tmp) return tmp;

    tmp = x.sortIndex - y.sortIndex;
    if (tmp) return tmp;

    if (x.name !== undefined) {
      if (y.name !== undefined) {
        tmp = x.name.localeCompare(y.name);
      } else {
        tmp = -1;
      }
    } else if (y.name !== undefined) {
      tmp = 1;
    }
    if (tmp) return tmp;

    return x.tid - y.tid;
  };

  return {
    Thread,
  };
});
</script>
